package com.readfeed.app;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.TimeZone;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import android.text.Html;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.ImageSpan;
import android.util.Xml;
import com.readfeed.app.model.Channel;
import com.readfeed.app.model.Feed;

public class ChannelParser {
	
	//tags
	private static final String TAG_FEED = "feed";
	private static final String TAG_TITLE = "title";
	private static final String TAG_LINK = "link";
	private static final String TAG_RSS = "rss";
	private static final String TAG_SUBTITLE = "subtitle";
	private static final String TAG_ENTRY = "entry";
	private static final String TAG_ITEM = "item";
	private static final String TAG_ID = "id";
	private static final String TAG_UPDATE = "updated";
	private static final String TAG_SUMMARY = "summary";
	private static final String TAG_DESC = "description";
	private static final String TAG_GUID = "guid";
	private static final String TAG_PUBDATE = "pubDate";
	private static final String TAG_CHANNEL = "channel";
	
	//attributes
	private static final String ATTR_REL = "rel";
	private static final String ATTR_HREF = "href";
	private static final String ATTR_ALTERNATE = "alternate";
	
	private static final Set<String> NAMESPACES = new HashSet<String>(Arrays.asList(new String[] {
			"","http://www.w3.org/2005/Atom","http://purl.org/dc/elements/1.1/"}));
	
	private static final String DATE_FORMATS[] = {"EEE, dd MMM yyyy HH:mm:ss Z",
		"EEE, dd MMM yyyy HH:mm:ss z", "EEE MMM dd HH:mm:ss zzz yyyy", "yyyy-MM-dd'T'HH:mm:ssz","yyyy-MM-dd'T'HH:mm:ssZ","yyyy-MM-dd'T'HH:mm:ss'Z'","yyyy-MM-dd'T'HH:mm:ss.SSSZ"};
	
	private SimpleDateFormat mSimpleDateFormats[] = new SimpleDateFormat[DATE_FORMATS.length];
	private SimpleDateFormat mNewDateFormat = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy", Locale.US);
	
	private Channel mChannel;
	private Feed mFeedItem;
	private String namespace = null;
	
	public ChannelParser(){
		for (int i = 0; i < DATE_FORMATS.length; i++) {
			//If Locale.US parameter is not set, a parsing error occurs on published date when device locale is set to a language other than English.			
			mSimpleDateFormats[i] = new SimpleDateFormat(DATE_FORMATS[i], Locale.US);
			mSimpleDateFormats[i].setTimeZone(TimeZone.getTimeZone("GMT"));
        }
	}

	public Channel handleChannel(URL url) throws IOException, XmlPullParserException, java.text.ParseException {
		InputStream stream = null;
		mChannel = new Channel();
		try{
			stream = this.downloadUrl(url);
			this.parse(stream);
			Date now = new Date();
			mChannel.setUpdateTime(now.getTime());
			return mChannel;
		} finally{
			if (stream != null) {
				stream.close();
			}
		}
	}
	
	private void parse(InputStream in) throws IOException, XmlPullParserException, java.text.ParseException {
		try{
			XmlPullParser parser = Xml.newPullParser();
			parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
			parser.setInput(in, null);
			parser.nextTag();
			this.parseChannel(parser);			
		} finally{
			in.close();
		}
	}
	
	private void parseChannel(XmlPullParser parser) throws XmlPullParserException, IOException, java.text.ParseException {		
		String tagName = parser.getName();
		String namespace = parser.getNamespace().trim();
		if (!(NAMESPACES.contains(namespace))) {		
			throw new XmlPullParserException("Invalid feed");
		}
		
		if (!tagName.equals(ChannelParser.TAG_FEED) && !tagName.equals(ChannelParser.TAG_RSS)) {
			throw new XmlPullParserException("Invalid feed");
		}
		
		if (tagName.equals(ChannelParser.TAG_FEED)) {
			this.parseAtom(parser);
		} else if (tagName.equals(ChannelParser.TAG_RSS)) {
			parser.nextTag();
			this.parseRSS(parser);
		}
	}
	
	private void parseRSS(XmlPullParser parser) throws XmlPullParserException, IOException, java.text.ParseException{
		parser.require(XmlPullParser.START_TAG, namespace, ChannelParser.TAG_CHANNEL);
		while (parser.next() != XmlPullParser.END_TAG) {
			if (parser.getEventType() != XmlPullParser.START_TAG) {
				continue;
			}
			
			String tagName = parser.getName();
			if (tagName.equalsIgnoreCase(ChannelParser.TAG_TITLE)) {
				String title = this.readText(parser);
				mChannel.setTitle(title);
				
				parser.require(XmlPullParser.END_TAG, namespace, ChannelParser.TAG_TITLE);
			} else if (tagName.equalsIgnoreCase(ChannelParser.TAG_DESC)) {
				String description = this.readText(parser);
				mChannel.setDescription(removeContentSpanObjects(description).toString());
				
				parser.require(XmlPullParser.END_TAG, namespace, ChannelParser.TAG_DESC);
			} else if (tagName.equalsIgnoreCase(ChannelParser.TAG_ITEM)) {
				this.parseRssItem(parser);
				mChannel.getItems().add(mFeedItem);
				
				parser.require(XmlPullParser.END_TAG, namespace, ChannelParser.TAG_ITEM);
			} else {
				this.skip(parser);
			}
		}
	}	
	
	private void parseAtom(XmlPullParser parser) throws XmlPullParserException, IOException, java.text.ParseException {		
		
		parser.require(XmlPullParser.START_TAG, namespace, ChannelParser.TAG_FEED);
		while (parser.next() != XmlPullParser.END_TAG) {
			if (parser.getEventType() != XmlPullParser.START_TAG) {
				continue;
			}
			
			String tagName = parser.getName();
			if (tagName.equalsIgnoreCase(ChannelParser.TAG_TITLE)) {
				String title = this.readText(parser);
				mChannel.setTitle(title);
				
				parser.require(XmlPullParser.END_TAG, namespace, ChannelParser.TAG_TITLE);
			} else if (tagName.equalsIgnoreCase(ChannelParser.TAG_SUBTITLE)) {
				String description = this.readText(parser);
				mChannel.setDescription(description);
				
				parser.require(XmlPullParser.END_TAG, namespace, ChannelParser.TAG_SUBTITLE);
			} else if (tagName.equalsIgnoreCase(ChannelParser.TAG_ENTRY)) {
				this.parseAtomEntry(parser);
				mChannel.getItems().add(mFeedItem);
				
				parser.require(XmlPullParser.END_TAG, namespace, ChannelParser.TAG_ENTRY);
			} else {
				this.skip(parser);
			}
		}
	}
	
	private void parseRssItem(XmlPullParser parser) throws XmlPullParserException, IOException, java.text.ParseException{
		mFeedItem = new Feed();
		while (parser.next() != XmlPullParser.END_TAG) {
			if (parser.getEventType() != XmlPullParser.START_TAG) {
				continue;
			}
			
			String tagName = parser.getName();
			if (tagName.equalsIgnoreCase(ChannelParser.TAG_TITLE)) {
				String title = this.readText(parser);
				mFeedItem.setTitle(title);
				
				parser.require(XmlPullParser.END_TAG, namespace, ChannelParser.TAG_TITLE);
			} else if (tagName.equalsIgnoreCase(ChannelParser.TAG_GUID)) {
				String guid = this.readText(parser);
				mFeedItem.setGuid(guid);
				
				parser.require(XmlPullParser.END_TAG, namespace, ChannelParser.TAG_GUID);
			} else if (tagName.equalsIgnoreCase(ChannelParser.TAG_LINK)) {
				String url = this.readText(parser);
				mFeedItem.setUrl(url);
				
				parser.require(XmlPullParser.END_TAG, namespace, ChannelParser.TAG_LINK);
			} else if (tagName.equalsIgnoreCase(ChannelParser.TAG_PUBDATE)) {
				Date pubDate = this.parseDate(parser);
				mFeedItem.setPubDate(pubDate);
				
				parser.require(XmlPullParser.END_TAG, namespace, ChannelParser.TAG_PUBDATE);
			} else if (tagName.equalsIgnoreCase(ChannelParser.TAG_DESC)) {
				String summary = this.readText(parser);
				mFeedItem.setSummary(removeContentSpanObjects(summary).toString());
				
				parser.require(XmlPullParser.END_TAG, namespace, ChannelParser.TAG_DESC);
			} else {
				this.skip(parser);
			}					
		}
		
		if (mFeedItem.getPubDate() == null) {
			Date pubDate = this.setPubDate();
			mFeedItem.setPubDate(pubDate);
		}			
		
		if(mFeedItem.getGuid() == null){
			String guid = mFeedItem.getUrl();
			mFeedItem.setGuid(guid);
		}
	}
	
	private void parseAtomEntry(XmlPullParser parser) throws XmlPullParserException, IOException, java.text.ParseException{
		
		mFeedItem = new Feed();
		while (parser.next() != XmlPullParser.END_TAG) {
			if (parser.getEventType() != XmlPullParser.START_TAG) {
				continue;
			}
			
			String tagName = parser.getName();
			if (tagName.equalsIgnoreCase(ChannelParser.TAG_TITLE)) {
				String title = this.readText(parser);
				mFeedItem.setTitle(title);
				
				parser.require(XmlPullParser.END_TAG, namespace, ChannelParser.TAG_TITLE);
			} else if (tagName.equalsIgnoreCase(ChannelParser.TAG_ID)) {
				String id = this.readText(parser);
				mFeedItem.setGuid(id);
				
				parser.require(XmlPullParser.END_TAG, namespace, ChannelParser.TAG_ID);
			} else if (tagName.equalsIgnoreCase(ChannelParser.TAG_LINK)) {			
				String link = "";
				String relType = parser.getAttributeValue(null, ChannelParser.ATTR_REL);
				if (relType.equalsIgnoreCase(ChannelParser.ATTR_ALTERNATE)) {
					link = parser.getAttributeValue(null, ChannelParser.ATTR_HREF);	
					mFeedItem.setUrl(link);
				}
				
				parser.nextTag();
				parser.require(XmlPullParser.END_TAG, namespace, ChannelParser.TAG_LINK);
			} else if (tagName.equalsIgnoreCase(ChannelParser.TAG_UPDATE)) {
				Date pubDate = this.parseDate(parser);
				mFeedItem.setPubDate(pubDate);
				
				parser.require(XmlPullParser.END_TAG, namespace, ChannelParser.TAG_UPDATE);
			} else if (tagName.equalsIgnoreCase(ChannelParser.TAG_SUMMARY)) {
				String summary = this.readText(parser);
				mFeedItem.setSummary(removeContentSpanObjects(summary).toString());
				
				parser.require(XmlPullParser.END_TAG, namespace, ChannelParser.TAG_SUMMARY);
			} else {
				this.skip(parser);
			}								
		}
		
		if (mFeedItem.getPubDate() == null) {
			Date pubDate = this.setPubDate();
			mFeedItem.setPubDate(pubDate);
		}	
		
		if(mFeedItem.getGuid() == null){
			String guid = mFeedItem.getUrl();
			mFeedItem.setGuid(guid);
		}
	}
	
	private Date parseDate(XmlPullParser parser) throws XmlPullParserException, java.text.ParseException, IOException{
		String dateString = this.readText(parser);
		Date result = null;
		for (int i = 0; i < DATE_FORMATS.length; i++) {
			try {
				result = mSimpleDateFormats[i].parse(dateString.trim());
				break;
			} catch (Exception e) {
				if (i == DATE_FORMATS.length-1) {
					result = setPubDate();
				}	
			}
        }
		
		return result;
	}
	
	private Date setPubDate() throws ParseException{
		Date now = new Date();
		return mNewDateFormat.parse(now.toString());
	}
	
	private void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
		int type = parser.getEventType();
	    if (type != XmlPullParser.START_TAG) {
	        throw new IllegalStateException();
	    }
	    
	    int depth = 1;
	    while (depth != 0) {
	        switch (parser.next()) {
	        case XmlPullParser.END_TAG:
	            depth--;
	            break;
	        case XmlPullParser.START_TAG:
	            depth++;
	            break;
	        }
	    }
	 }
	
	private String readText(XmlPullParser parser) throws XmlPullParserException, IOException{
		String result = "";
		if (parser.next() == XmlPullParser.TEXT) {
			result = parser.getText();
			parser.nextTag();
		}
		
		return result;
	}
	
	private InputStream downloadUrl(URL url) throws IOException{		
	    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
	    conn.setReadTimeout(10000);
	    conn.setConnectTimeout(15000);
	    conn.setRequestMethod("GET");
	    conn.setDoInput(true);
	    conn.connect();
	    InputStream stream = conn.getInputStream();
	    
	    return stream; 
	}
	
	private Spanned removeContentSpanObjects(String summary) {
		SpannableStringBuilder spannedStr = (SpannableStringBuilder)Html.fromHtml(summary.trim());
		Object[] spannedObjects = spannedStr.getSpans(0,spannedStr.length(),Object.class);
		for (int i = 0; i < spannedObjects.length; i++) {
			if (spannedObjects[i] instanceof ImageSpan)
				spannedStr.replace(spannedStr.getSpanStart(spannedObjects[i]),
						spannedStr.getSpanEnd(spannedObjects[i]), "");			
		}	

		return spannedStr;
	}
}